Skip to content

feat: add the VAC and VDC credential types - #15

Merged
stormer78 merged 1 commit into
mainfrom
feat/vac-vdc-credentials
Sep 2, 2026
Merged

feat: add the VAC and VDC credential types#15
stormer78 merged 1 commit into
mainfrom
feat/vac-vdc-credentials

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Track 0 of the data-rooms delivery plan. Unblocks the demo and the rooms/* work, and
depends on nothing — this is our library, and DTGCredentialType is already
#[non_exhaustive] with a new_v* constructor per type, so this is the existing pattern
rather than a fork.

Both types track drafts — trustoverip/dtgwg-cred-spec
#29 (VAC) and
#19 (VDC) — and say so in their
API docs, so a consumer knows the shape may move before those are approved.

authority::verify_chain is the part that matters

Everything else here is a struct and a signature. Anyone can mint a well-formed VAC
naming any scope and any actions, and it will verify perfectly as a credential.
What makes
it worthless is that its chain does not reach the party governing the scope — so a verifier
that checks only the credential it was handed accepts a self-issued grant of arbitrary
authority.

Seven rules, each closing one way of getting more than was granted:

Rule What it stops
Chain must reach a root issued by the governing party a self-issued grant
No link may add an action absent from its parent escalation by re-issue
No link may widen scope authority earned in one room used in another
No link may outlive its parent an expiry escaped by re-delegation
Each link's issuer must be its parent's subject grafting someone else's grant onto your own
audience, where set, must be the presenter a leaked credential used by whoever holds it
Depth bounded at 8 DoS against a verifier that walks every link

Resolution is bearer-side by construction. verify_chain takes the whole chain as a
slice and never dereferences parent. Resolving over the network would make verification
depend on availability, turn every id into a request the verifier can be induced to make
against an address the holder chooses, and signal credential use to whoever hosts the
identifier. id values are identifiers, not locators.

What attenuate buys

The case the credential exists for: a member holds read/write/curate for a month;
their agent runs on read, for four hours, bound to the agent's own DID. Derived by the
holder with no round trip to the issuer, and refused at issue time if it would widen — while
the verifier's check remains authoritative, since nothing stops a different implementation
emitting the JSON by hand.

Tests are mostly attacks

14 in tests/authority_chain.rs, and the interesting ones are the refusals: a self-issued
grant, an added action, a grafted parent, an audience-bound credential presented by someone
else, an expired link beneath a live parent, a chain past the depth ceiling, and an empty
actions list — which is refused both by the constructor and at the deserialization
boundary, since a guard only on the constructor is trivially bypassed by serde_json.

cargo test 73 passing · cargo clippy --all-targets clean · cargo fmt applied.

Version

0.5.00.6.0. DTGCredentialType is #[non_exhaustive], so callers matching with a
wildcard arm are unaffected; callers matching exhaustively need one arm each.

Adds the two credentials that confer rather than assert. Both track drafts
in trustoverip/dtgwg-cred-spec - PR #29 (VAC) and #19 (VDC) - and are
marked as such in the API docs so a consumer knows the shape may move.

The VAC carries an AuthorityGrant: a scope, the actions permitted within
it, and the optional parent and audience that make attenuation work.
attenuate() derives a narrower VAC from one the holder already has,
without involving the issuer - which is what lets a member equip an agent
with four hours of read-only access rather than lending it their own
standing authority.

authority::verify_chain is the part that matters. Issuing a VAC is a
struct and a signature; what stops a holder acquiring authority they were
never given is a verifier refusing a chain that widens. Anyone can mint a
well-formed VAC naming any scope and any actions and it will verify
perfectly as a credential - what makes it worthless is that its chain does
not reach the party governing the scope. Seven rules, each closing one way
of getting more than was granted, and the tests are mostly attacks: a
self-issued grant, an added action, a grafted parent, a leaked
audience-bound credential, an expired link under a live parent, a chain
past the depth ceiling.

Resolution is bearer-side by construction - verify_chain takes the chain
as a slice and never dereferences parent. Resolving over the network would
make verification depend on availability, turn every id into a request the
verifier can be induced to make against an address the holder chooses, and
signal credential use to whoever hosts the identifier.

Empty actions confers nothing rather than everything, refused both by the
constructor and at the deserialization boundary so it cannot be reached
either way.

DTGCredentialType is non_exhaustive, so the new variants do not break
callers matching with a wildcard.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@affinidi-appsecurity-bot

affinidi-appsecurity-bot commented Sep 2, 2026

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

2 AI-confirmed issues, 3 findings need a human to review/validate.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #15

Field Value
Repository OpenVTC/dtg-credentials
Branch feat/vac-vdc-credentialsmain
Validated 2026-09-05
Scan ID f8b9c34c
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 3 · with findings: 1 · files: 6 · findings: 8

Module Files scanned Findings
src 3 8
(root) 2 0
tests 1 0

Executive Summary

Category Confirmed Must-Review-By-Human
Security Issues 2 3

⚠️ 3 finding(s) need human review. These could not be conclusively confirmed or dismissed automatically (insufficient evidence). They are not dismissed — a developer / security team member must read and decide.


🔒 Security Issues

Confirmed Vulnerabilities (2)

🟡 Validity-window checks trust a fully caller-supplied timestamp, enabling temporal bypass if at is attacker-influenced

Field Detail
Severity MEDIUM
Location src/authority.rs:216
Finding ID github_pr-1199860b7fd1
CWE CWE-294, CWE-367
OWASP A07:2021 - Identification and Authentication Failures
MITRE ATT&CK T1078 - Valid Accounts, T1553 - Subvert Trust Controls
CAPEC CAPEC-90, CAPEC-462
DREAD 6.4
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: HIGH → MEDIUM — The scanner itself rates exploitability as 'medium' and confirms no direct injection vector into at exists in the visible code — exploitation is 'entirely conditional on caller behavior outside this file.' No public exploit exists, exploit maturity is only 'conceptual', and CVSS is not provided (unable to confirm CVSS≥7 threshold). Per HIGH severity gates, this requires exploitability≥5 OR public_exploit≥7, and while exploitability is borderline-medium, the lack of confirmed production deployment and lack of a demonstrated attacker-controlled path into at within this codebase caps this at medium. This is a legitimate design weakness worth fixing but not an immediately exploitable vulnerability as currently evidenced.
  • Composite score: 4.4
  • Environment: unknown

Summary: verify_chain's temporal validity checks are only as trustworthy as the at parameter passed in by the caller; the function itself performs no freshness or monotonicity enforcement on this value, creating a design-level risk that any integrator sourcing at from client-controlled or cached data creates a replay/rollback vector for expired credentials.

📝 Description:

In deployments where the at timestamp is derived from client-supplied data, a cached authorization decision, or any source outside a trusted, freshly-sampled system clock, an attacker can present an expired VAC/VDC chain together with a stale at value and have the library report it as currently valid, extending access beyond the intended validity window.

🧪 Proof of Concept:

Both comparisons are performed strictly against the at argument as given, with no internal call to Utc::now() and no check that at is recent/fresh relative to any trusted reference. Any caller-controlled or replayed at value that predates the credential's real expiry will cause these checks to pass for an otherwise-expired credential.

        let c = link.credential();
        if c.valid_from() > at {
            return Err(AuthorityError::NotValidNow { index, at });
        }
        if let Some(until) = c.valid_until()
            && until < at
        {
            return Err(AuthorityError::NotValidNow { index, at });
        }
    }

Vulnerable lines: 213, 226

🔎 Evidence: src/authority.rs:216

if c.valid_from() > at {
    return Err(AuthorityError::NotValidNow { index, at });
}
if let Some(until) = c.valid_until()
    && until < at
{
    return Err(AuthorityError::NotValidNow { index, at });
}

💥 Impact:

In deployments where the at timestamp is derived from client-supplied data, a cached authorization decision, or any source outside a trusted, freshly-sampled system clock, an attacker can present an expired VAC/VDC chain together with a stale at value and have the library report it as currently valid, extending access beyond the intended validity window.

Confidentiality: High — an expired credential that should be denied is instead accepted, extending unauthorized access beyond its intended lifetime · Integrity: Low — does not itself corrupt data, but permits actions under a false authorization state · Availability: None

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: none
  • Attack path: EP-001 (verify_chain at parameter) ← caller passes any DateTime, potentially derived from untrusted input

⚖️ Triage Factors:

Factor Value
Fixable ✅ Yes
Exploitability medium
Business impact medium
Public exploit None known
Environment unknown

Attack scenario: If an integrator derives the at argument to verify_chain from anything other than a freshly-sampled trusted clock, an attacker can present an already-expired chain alongside a stale/replayed at value and have it accepted as currently valid.

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Removing at from the public parameter list and sampling it internally with Utc::now() eliminates any possibility of a caller supplying a stale, replayed, or attacker-influenced timestamp. If deterministic time injection is required for testing, expose it only behind a #[cfg(test)]-gated or explicitly-named verify_chain_at variant, never as the default production entry point.

Vulnerable code:

pub fn verify_chain(
    chain: &[DTGCredential],
    governing_party: &str,
    requested_scope: &str,
    requested_action: &str,
    presenter: &str,
    at: DateTime<Utc>,
) -> Result<VerifiedAuthority, AuthorityError> { ... uses `at` directly for all validity checks ... }

Secure code:

pub fn verify_chain(
    chain: &[DTGCredential],
    governing_party: &str,
    requested_scope: &str,
    requested_action: &str,
    presenter: &str,
) -> Result<VerifiedAuthority, AuthorityError> {
    let at = Utc::now(); // sampled internally; never caller-influenced
    // ... rest unchanged, using this trusted `at` ...
}

Additional recommendations:

  • If a testable variant with explicit at must remain for unit tests, name it distinctly (e.g. verify_chain_at) and mark it #[doc(hidden)] or behind a feature flag so production code paths cannot easily reach for it.
  • Add monotonic clock / replay-protection guidance for any calling service that must cache authorization decisions.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: verify_chain signature is pub fn verify_chain(chain: &[DTGCredential], governing_party: &str, requested_scope: &str, requested_action: &str, presenter: &str, at: DateTime<Utc>) -> Result<VerifiedAuthority, AuthorityError>at is a plain caller-supplied parameter, not derived internally via Utc::now(). The validity checks are: if c.valid_from() > at { return Err(...) } and if let Some(until) = c.valid_until() && until < at { return Err(...) } (lines 216-225 per finding,
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 Non-canonicalized string equality on identity fields (issuer/subject/audience) risks spoofing if identifiers are DIDs/URIs with encoding variance

Field Detail
Severity MEDIUM
Location src/authority.rs:296
Finding ID github_pr-23a5230f7f1c
CWE CWE-178, CWE-697
OWASP A07:2021 - Identification and Authentication Failures
MITRE ATT&CK T1036 - Masquerading
CAPEC CAPEC-627, CAPEC-165
Reachability 🔴 Reachable
Exploit Maturity theoretical
Detection Source skill_scan

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • CWE-178/697 comparison-normalization issue confirmed present in code (src/authority.rs:296-301) and reachable via unauthenticated EP-001, but exploit maturity is only 'theoretical' with no demonstrated encoding-variance exploit and unknown production status. This does not meet high/critical bar (requires exploit maturity ≥ PoC or confirmed prod exposure with concrete attack demonstration); it exceeds low because the flaw is real, reachable, and in an identity/authorization-adjacent security boundary. Medium is the accurate calibration.
  • Composite score: 4.3
  • Environment: unknown

🔎 Evidence: src/authority.rs:296

if root.credential().issuer() != governing_party {
    return Err(AuthorityError::RootNotGoverning { ... });
}

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: none
  • Attack path: EP-001 (verify_chain) — issuer/subject/audience/id fields of attacker-supplied DTGCredential objects flow directly into ==/!= comparisons with no canonicalization step

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: if root.credential().issuer() != governing_party (RootNotGoverning check), if link.credential().issuer() != parent.credential().subject() (IssuerNotParentSubject), if audience != presenter (WrongAudience), and match (&grant.parent, parent.id()) { (Some(named), Some(presented)) if named == presented => {} ... } (BrokenLink) — all confirmed in src/authority.rs as plain Rust !=/== string comparisons with no visible normalization, case-folding, or Unicode canonicalization
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

⚠️ Must-Review-By-Human (3)

Validated up to a point, but inconclusive — a human must read the code and make the final call. Reported (not dismissed) so developers and the security team receive them.

🟠 Hardcoded Secrets: Generic Api Key (2 occurrences in 1 unique secrets)

Field Detail
Severity HIGH
Location src/lib.rs:923
Finding ID github_pr-ecd571317032
CWE CWE-798
OWASP A07:2021 - Identification and Authentication Failures
CVSS 4.0 8
Exploit Maturity conceptual
Detection Source mcp_gitleaks

Summary: 1 unique generic api key secret(s) detected across 2 location(s). Locations: lib.rs:923, lib.rs:955

📝 Description:

Detected 1 unique generic api key secret(s) across 2 locations. These hardcoded secrets could expose access to services and sensitive operations.

🌱 Root Cause: Hardcoded Generic Api Key in source code

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Priority: Immediate

Remove 2 hardcoded generic api key secret(s) and use environment variables or a secure vault instead.

Also flagged at this location (same code, other weakness framings): UNCONFIRMED — Hardcoded Secret: Generic Api Key in lib.rs:923

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 85%
  • AI Validation Evidence: EVIDENCE FOUND: Finding claims a generic API key secret at src/lib.rs:923 (2 occurrences). The src/lib.rs diff hunks provided show additions related to AuthorityGrant, DTGCredentialError variants, DTGCredentialType, and DTGCommon::authority()/authority_mut() — none of the visible hunks contain anything resembling an API key literal. Line 923 is beyond the visible diff hunks (largest visible line reference is ~640s). EVIDENCE NOT FOUND: The actual byte content at src/lib.rs:923 was not included i
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.

⚪ UNCONFIRMED — Hardcoded Secret: Generic Api Key in lib.rs:955

Field Detail
Severity INFORMATIONAL
Location src/lib.rs:955
Finding ID github_pr-70c3b18596b7
CWE CWE-798
OWASP A07:2021 - Identification and Authentication Failures
CAPEC CAPEC-561
Reachability 🔴 Reachable
Exploit Maturity theoretical
Detection Source skill_scan

Summary: Cannot confirm or deny this finding. The tool flagged a 'Generic Api Key' pattern in src/lib.rs at line 955, but src/lib.rs's actual content is not present anywhere in the sourceCode, diff, or priorArtifacts supplied for this task. Per instructions, only confirmed true positives should be reported with full evidence — this finding lacks the code evidence required for confirmation.

📝 Description:

Cannot be assessed without confirming whether a real secret is present. If confirmed, and given this is a publish = true crate per Cargo.toml, any embedded real secret would become permanently public on crates.io upon the next cargo publish, and would already be exposed in git history if committed.

🧪 Proof of Concept:

No PoC can be constructed without the actual source line and surrounding context. Fabricating a code snippet would misrepresent the codebase and violate evidence-based reporting requirements.

[UNAVAILABLE — file not provided in this analysis session; cannot show surrounding context]

Vulnerable lines: 955, 955

🔁 Reproduction Steps:

  1. Obtain the actual content of src/lib.rs from the repository at the relevant commit.
  2. Inspect line 955 and surrounding context (±10 lines) for the matched literal.
  3. Cross-check whether this occurrence and the one at line 923 belong to the same struct/enum definition (e.g., two related fields in a credential subject type, given the crate's domain of DTG credentials) rather than two independent secrets.
  4. Classify as true or false positive only after direct inspection; if true, treat as an active incident per remediation guidance below.

🔎 Evidence: src/lib.rs:955

[NOT PROVIDED — src/lib.rs content absent from all supplied artifacts]

💥 Impact:

Cannot be assessed without confirming whether a real secret is present. If confirmed, and given this is a publish = true crate per Cargo.toml, any embedded real secret would become permanently public on crates.io upon the next cargo publish, and would already be exposed in git history if committed.

🧭 Reachability:

  • Network exposure: unknown
  • Auth barrier: unknown
  • Attack path: unknown — cannot trace without src/lib.rs content

⚖️ Triage Factors:

Factor Value
Fixable ✅ Yes
Exploitability unknown
Business impact unknown
Public exploit None known
Environment unknown

Attack scenario: Unconfirmed — gitleaks flagged a second pattern-matched string at src/lib.rs:955 as a possible generic API key, but the actual file content was never supplied, so no realistic exploitation scenario can be grounded in evidence.

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Same remediation path as VULN-MCP-001: confirm via direct inspection first; if real, rotate the credential, purge from history, and replace with environment-variable or secrets-manager-backed configuration.

Vulnerable code:

[UNKNOWN — requires src/lib.rs content]

Secure code:

[UNKNOWN — requires src/lib.rs content]

Additional recommendations:

  • Cross-reference both flagged lines (923 and 955) together when inspecting src/lib.rs, since they may share a root cause (e.g., a shared example/test block or a repeated pattern in derive macro output).
  • Add/verify gitleaks runs in CI pre-merge so any future reintroduction is caught before merge rather than post-hoc.
  • If false positive (e.g., a field named api_key: String in a #[derive(Serialize, Deserialize)] struct with no literal secret value), add a scoped gitleaks allowlist entry with a comment explaining why, rather than disabling the rule globally.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: This finding self-reports as UNCONFIRMED, noting src/lib.rs content is absent from all provided artifacts at line 955. EVIDENCE NOT FOUND: Full src/lib.rs content at line 955; the diff hunks provided end well before line 955 (visible hunks reference up to roughly line 640s in the new lib.rs additions). CHANGED VS PRE-EXISTING: src/lib.rs is in the changed-files list making it in-scope, but the specific line content cannot be confirmed. VERDICT JUSTIFICATION: Same reasoning as the
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.


Generated by Agentic Sec — AI Security Validation Agent
This report includes full scan data + AI validation evidence. Feed to engineering copilots for automated fix deployment.

Complementary: 🛡️ **Threat Model & Affect Analysis**
Details

🛡️ Threat Model & Affect Analysis — PR #15

Field Value
Repository OpenVTC/dtg-credentials
Branch feat/vac-vdc-credentialsmain
Generated 2026-09-05

ℹ️ This report contains theoretical threats and impact analysis for the MR.
Unlike the Security Code Review Report (which contains confirmed, materialised issues),
these are potential risks that may or may not be exploitable. Use this for defence-in-depth planning.


📋 Affect Analysis

Change Summary

This PR introduces two new credential types — Verifiable Authority Credential (VAC) and Verifiable Delegation Credential (VDC) — along with a new authority-chain verification module (src/authority.rs) that enforces attenuation-only (narrowing) delegation semantics. The core security value is verify_chain, which prevents self-issued/escalated authority grants by requiring a chain to trace back to a governing-party-issued root and enforcing seven narrowing invariants at every link.

Diff: +641 / -6 lines
Types: feature, security, docs, config

🧩 Affected Components

Component Impact Change What Changed
Authority Chain Verifier (authority.rs) critical new Introduces verify_chain, AuthorityError, VerifiedAuthority, and MAX_CHAIN_DEPTH — a complete new authorization-decision subsystem for valida
Credential Construction & Attenuation (create.rs) high new Adds new_vac, new_vdc, and attenuate constructors following the existing new_v* pattern, producing DTGCredential objects consumed by verify_

📁 File Classifications

src/authority.rs

  • Type: security

src/create.rs

  • Type: security

CHANGELOG.md

  • Type: docs

Cargo.toml

  • Type: config

tests/authority_chain.rs

  • Type: test

🛡️ STRIDE Threat Model

Identified Threats (12)

🔴 STRIDE-1: Signature Verification Omission in verify_chain Consumer Integration

Field Detail
Category Spoofing, Elevation of Privilege
Severity Critical
Likelihood Likely
CVSS 9.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity High
CWE CWE-345,CWE-347,CWE-306
CAPEC CAPEC-115,CAPEC-194
OWASP A07:2021 - Identification and Authentication Failures, A01:2021 - Broken Access Control

Description: authority::verify_chain in COMP-001 allows complete authority forgery due to the function explicitly not verifying cryptographic signatures on any credential in the chain, resulting in unauthorized privilege grants if a caller invokes verify_chain without independently verifying signatures first

Evidence: src/authority.rs:~204-220

/// The signature on each credential is *not* checked here. Verify those first, with\n/// [crate::DTGCredential] and the data-integrity suite; ...\npub fn verify_chain(\n    chain: &[DTGCredential],\n    governing_party: &str,\n    ...\n) -> Result<VerifiedAuthority, AuthorityError> {

Attack Scenario:

  1. Attacker crafts an arbitrary DTGCredential chain in JSON with a fabricated root claiming issuer == governing_party and a leaf granting themselves broad actions/scope, without ever holding a valid private key.
  2. Attacker's integrating application deserializes the JSON into Vec<DTGCredential> and passes it directly to verify_chain(chain, governing_party, requested_scope, requested_action, presenter, at) in src/authority.rs without first calling the separate data-integrity/signature verification suite mentioned only in the doc comment ('The signature on each credential is not checked here...').
  3. verify_chain walks the structural rules only — BrokenLink, IssuerNotParentSubject, WidensScope/Actions, OutlivesParent — none of which touch cryptographic proof, and the attacker-controlled chain satisfies every structural invariant because the attacker constructs both leaf and root to be self-consistent.
  4. RootNotGoverning check at the bottom compares root.credential().issuer() != governing_party as a plain string equality on an unauthenticated claim field, which the attacker sets verbatim to the expected governing party name.
  5. verify_chain returns Ok(VerifiedAuthority{...}), and the calling application grants the requested access believing the chain was cryptographically valid.
  6. Attacker has now obtained full impersonation of the governing party's issued authority with zero valid signatures.

🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001

  • Data Flows: DF-authority-chain-verification

Preconditions: Integrating application calls verify_chain without a preceding mandatory signature-verification step, Attacker can supply arbitrary DTGCredential JSON/objects as chain input, No structural or type-level enforcement in this module linking a 'verified' credential type to a 'checked' state

Existing Controls: Extensive doc comment in src/authority.rs explicitly warning integrators that signatures are not checked here and must be verified separately

Recommended Mitigations: Introduce a type-state wrapper (e.g., VerifiedCredential<T>) that can only be constructed after signature verification, and require verify_chain to accept only that wrapped type instead of raw DTGCredential • Add a runtime assertion/callback hook inside verify_chain requiring a signature-verification closure to be passed and invoked per link • Add integration tests and CI checks in tests/authority_chain.rs (not shown) asserting that unsigned or invalid-signature chains are rejected end-to-end • Document this as a hard API contract with a #[must_use]-style compile-time gate rather than doc-comment-only guidance


🟠 STRIDE-2: String-Based Identity Comparison Enabling Homoglyph/Case Spoofing in RootNotGoverning Check

Field Detail
Category Spoofing, Elevation of Privilege
Severity High
Likelihood Possible
CVSS 7.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-178,CWE-697
CAPEC CAPEC-627,CAPEC-165
OWASP A07:2021 - Identification and Authentication Failures

Description: verify_chain in COMP-001 allows governing-party impersonation due to plain != string equality checks (RootNotGoverning, IssuerNotParentSubject, WrongAudience) on attacker-supplied identifier strings without normalization, resulting in a spoofed root or issuer bypassing the intended trust boundary if the identity strings are DIDs/URIs subject to case, Unicode, or percent-encoding variance

Evidence: src/authority.rs:~296-301

if root.credential().issuer() != governing_party {\n    return Err(AuthorityError::RootNotGoverning { ... });\n}

Attack Scenario:

  1. Governing party's canonical identifier is registered/expected as e.g. did:web:Example.com in the caller's governing_party parameter.
  2. Attacker issues (or claims to issue) a root credential where issuer field is a visually or byte-level distinct but application-equivalent string (differing case, trailing slash, percent-encoded characters, or Unicode confusable) that the upstream identity resolver treats as equivalent but Rust's != treats as different — or conversely, if the comparison side used by the caller is inconsistent, the two-sided mismatch could be exploited to smuggle a distinct-but-equivalent issuer past logging/detection while still passing the literal check used elsewhere in the codebase.
  3. if root.credential().issuer() != governing_party in src/authority.rs performs raw PartialEq on &str/String with no canonicalization (case-folding, Unicode NFC, DID method-specific normalization).
  4. Similarly, IssuerNotParentSubject and WrongAudience checks use the same raw equality against attacker-controlled issuer/subject/audience fields propagated from deserialized JSON.
  5. Depending on how the surrounding system resolves/compares these identifiers elsewhere (e.g., DID resolution, case-insensitive lookups in a directory), an attacker can craft a credential chain whose string fields pass verify_chain's literal comparison while representing a different real-world identity than intended, or vice versa causing legitimate chains to be wrongly rejected (functional/DoS side-effect).

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: DF-authority-chain-verification

Preconditions: Identity strings (issuer/subject/audience/governing_party) are DIDs or URIs with a normalization scheme not enforced before comparison, Attacker controls or influences credential JSON fields prior to verify_chain invocation, Some other component in the system treats two distinct string representations as the same identity

Existing Controls: Use of typed String fields with PartialEq provides deterministic (if strict) comparison • thiserror-based error variants make mismatches visible in logs for post-hoc detection

Recommended Mitigations: Normalize all identity strings (case-folding, NFC Unicode normalization, DID-method-specific canonicalization) before any equality comparison in verify_chain • Introduce a dedicated Did/Identifier newtype with a custom PartialEq enforcing canonical comparison • Add fuzz/property tests feeding Unicode-confusable and case-variant identifiers into verify_chain • Document the exact identifier format contract expected by verify_chain callers


🟡 STRIDE-3: Algorithmic Complexity Amplification via Repeated Nested Action-List Scan in Chain Walk

Field Detail
Category Denial of Service
Severity Medium
Likelihood Possible
CVSS 5.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-405,CWE-1050
CAPEC CAPEC-131,CAPEC-25
OWASP A04:2021 - Insecure Design

Description: verify_chain in COMP-001 allows moderate resource exhaustion due to an O(depth × actions²) nested .contains() scan over attacker-controlled actions Vec at each of up to MAX_CHAIN_DEPTH=8 links, resulting in increased per-call CPU cost when actions lists are maximally sized (bounded but non-trivial amplification, especially if actions strings are also long)

Evidence: src/authority.rs:~275-281

for action in &grant.actions {\n    if !parent_grant.actions.contains(action) {\n        return Err(AuthorityError::WidensActions { index, action: action.clone() });\n    }\n}

Attack Scenario:

  1. Attacker constructs a chain at the maximum permitted depth (8 links, per MAX_CHAIN_DEPTH) where every link's actions Vec contains a large number of long strings.
  2. Attacker ensures each actions list is crafted so that many entries must be checked before the !parent_grant.actions.contains(action) linear scan concludes (worst-case: no match found until end of list), repeated for every action in every link at every depth level.
  3. for action in &grant.actions { if !parent_grant.actions.contains(action) {...} } in src/authority.rs runs this nested linear scan once per link, with .contains() itself doing a String comparison per candidate — total cost scales with (depth × actions_per_link × actions_per_parent × avg_string_length).
  4. Attacker submits many such maximally-sized chains concurrently to a service exposing verify_chain (e.g., an API endpoint gating on EP-001), amplifying CPU consumption disproportionately to network/request cost, since chain size is not otherwise bounded beyond depth.
  5. Aggregate CPU exhaustion degrades verifier availability for legitimate credential holders.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: DF-authority-chain-verification

Preconditions: No upper bound enforced on the size of actions Vec per link or on string length within actions, verify_chain is reachable at sufficient request volume by an unauthenticated or lightly-authenticated caller, Attacker can supply crafted DTGCredential objects with large actions lists (constructor in src/create.rs only forbids empty lists, not oversized ones per visible diff)

Existing Controls: MAX_CHAIN_DEPTH = 8 bounds the number of links, limiting the outer multiplier • NoActions check rejects empty lists, but not oversized ones

Recommended Mitigations: Enforce a maximum length on actions Vec and a maximum string length per action at credential construction and/or deserialization boundary • Replace Vec<String> + linear .contains() with a HashSet<String> for O(1) membership checks when actions lists can be large • Add request-level rate limiting and payload-size limits ahead of verify_chain in the hosting service • Add a fuzz/benchmark test asserting verify_chain completes within an SLA bound for maximal-size adversarial input


🟡 STRIDE-4: Missing Duplicate/Cycle Detection Enabling Repeated-ID Chain Confusion

Field Detail
Category Tampering
Severity Medium
Likelihood Possible
CVSS 6.3 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-706,CWE-841
CAPEC CAPEC-141,CAPEC-462
OWASP A08:2021 - Software and Data Integrity Failures

Description: verify_chain in COMP-001 allows chain-integrity confusion due to the algorithm only checking adjacent-link parent-pointer consistency (BrokenLink) without validating global uniqueness of id values across the presented chain, resulting in potential logical inconsistency if an attacker supplies a chain containing repeated or self-referential id values that satisfy pairwise adjacency checks

Evidence: src/authority.rs:~247-263

match (&grant.parent, parent.id()) {\n    (Some(named), Some(presented)) if named == presented => {}\n    ...

Attack Scenario:

  1. Attacker builds a chain where chain[2].id() equals chain[0].id() (or some other non-adjacent duplication), while every adjacent pair still satisfies grant.parent == parent.id() for the immediate neighbor per the BrokenLink check in src/authority.rs.
  2. Because verify_chain only compares chain[index] against chain[index+1] (immediate neighbor), it never detects that an id reused elsewhere in the chain could represent the attacker inserting a link that structurally 'points backward' into an earlier position once combined with how a consuming system might cache or deduplicate credentials by id.
  3. If the surrounding system (not shown, in src/lib.rs or a credential store) indexes/caches credentials by id and later re-resolves them by that key rather than relying purely on the presented slice, the duplicate id could cause it to substitute a different (attacker-controlled) credential object at lookup time than the one verify_chain actually validated in-slice, breaking the 'never dereferences parent' invariant asserted in the module doc.
  4. This creates a verification/enforcement mismatch: verify_chain's guarantees apply only to the exact objects in the slice, but downstream code that re-keys by id could inadvertently substitute a different, unverified object, defeating the intended security model.

🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001

  • Data Flows: DF-authority-chain-verification

Preconditions: A calling system caches, indexes, or re-resolves credentials by id after verify_chain returns success, rather than acting only on the exact validated slice, Attacker can produce two distinct DTGCredential objects sharing the same id value, Chain length ≥ 3 so non-adjacent duplication is structurally possible within MAX_CHAIN_DEPTH

Existing Controls: BrokenLink check enforces strict adjacent parent/id linkage • Module doc explicitly states resolution is bearer-side and parent is never dereferenced by this function itself

Recommended Mitigations: Add an explicit uniqueness check across all id values in the presented chain before/while walking it, rejecting chains with duplicate ids • Document clearly that any caller must use only the exact validated slice object references post-verification and must not re-resolve by id • Add regression tests in tests/authority_chain.rs covering duplicate-id and cyclic chain inputs


🟠 STRIDE-5: Clock-Trust Manipulation via Caller-Supplied at Parameter Bypassing Validity Windows

Field Detail
Category Tampering, Elevation of Privilege
Severity High
Likelihood Likely
CVSS 7.6 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-294,CWE-367
CAPEC CAPEC-90,CAPEC-462
OWASP A08:2021 - Software and Data Integrity Failures

Description: verify_chain in COMP-001 allows expired/not-yet-valid credential acceptance due to the validity-window check trusting an entirely caller-supplied at: DateTime<Utc> parameter instead of an authoritative system clock, resulting in bypass of temporal authority constraints (OutlivesParent, NotValidNow) if the calling integration passes an attacker-influenced or stale timestamp

Evidence: src/authority.rs:~215-235

if c.valid_from() > at {\n    return Err(AuthorityError::NotValidNow { index, at });\n}\nif let Some(until) = c.valid_until()\n    && until < at\n{\n    return Err(AuthorityError::NotValidNow { index, at });\n}

Attack Scenario:

  1. Attacker holds an authority credential that has since expired (valid_until in the past relative to true current time) or a chain link whose parent has since expired.
  2. The integrating application derives the at parameter from an untrusted or attacker-influenceable source — e.g., a client-supplied request timestamp, a replayed cached value, or a server clock vulnerable to NTP manipulation — rather than always using Utc::now() freshly at verification time.
  3. Attacker submits (or replays) a request causing the caller to invoke verify_chain(chain, governing_party, scope, action, presenter, at) with an at value set to a moment when the now-expired credential was still valid.
  4. if c.valid_from() > at and if until < at checks in src/authority.rs pass because at reflects the attacker-favorable past instant rather than true present time.
  5. verify_chain returns Ok(VerifiedAuthority{...}) for a chain that should be expired, and the caller grants access based on stale authority.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: DF-authority-chain-verification

Preconditions: Calling application does not strictly bind at to a freshly-sampled, trusted Utc::now() at the moment of authorization decision, Attacker can influence or replay the at value supplied to verify_chain (e.g., via a request parameter, cached session state, or clock skew), Credential in question has since expired but was valid at the attacker-chosen at

Existing Controls: Validity window is checked per-link, not just once for the whole chain, narrowing (but not eliminating) the exposure window • OutlivesParent check prevents extending validity beyond the parent regardless of at

Recommended Mitigations: Remove at as a caller-supplied parameter and instead have verify_chain internally call Utc::now(), or clearly document that callers MUST pass only a freshly-sampled trusted timestamp and never persist/replay it • Add monotonic/replay protection at the calling layer to prevent reuse of a stale at value across multiple authorization decisions • Add integration tests asserting that credentials expired relative to true wall-clock time are rejected even under adversarial at manipulation attempts at the calling layer


🟡 STRIDE-6: Insufficient Structured Audit Logging of Rejected Authority Chains at verify_chain Boundary

Field Detail
Category Repudiation
Severity Medium
Likelihood Likely
CVSS 4.8 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N
Residual Severity Low
CWE CWE-778,CWE-223
CAPEC CAPEC-93
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: verify_chain in COMP-001 allows undetected repeated escalation attempts due to the function returning a typed Result<_, AuthorityError> with no built-in audit logging, structured event emission, or rate-limited alerting hook, resulting in a lack of non-repudiable evidence and delayed detection if a caller does not independently log every AuthorityError occurrence with sufficient context

Evidence: src/authority.rs:~55-60

/// Each variant names a specific way of acquiring authority that was not granted, rather\n/// than collapsing into one \"invalid\" — a verifier's logs are where an escalation attempt\n/// becomes visible.

Attack Scenario:

  1. Attacker repeatedly submits crafted chains attempting privilege escalation (e.g., WidensScope, WidensActions, RootNotGoverning) against a service embedding verify_chain.
  2. Each attempt returns a distinct, well-described AuthorityError variant (e.g., RootNotGoverning{root_issuer, expected}) from src/authority.rs, but the module itself performs no logging — it purely returns a value.
  3. If the calling application does not itself log every Err(AuthorityError::*) return with correlated identity (presenter, requested scope/action, timestamp) to an immutable/append-only audit trail, the attacker's escalation attempts leave no forensic trace beyond possibly a generic application error log.
  4. Attacker can iterate many chain variations (probing for the exact rule triggering rejection, since each error variant reveals precisely which invariant failed) without detection, using the granular error messages themselves as an oracle to refine subsequent forged chains.
  5. Without structured logging tied to identity, security teams cannot distinguish a single misconfigured legitimate client from a sustained, deliberate escalation-probing campaign.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: DF-authority-chain-verification

Preconditions: Calling application does not implement mandatory structured audit logging for every AuthorityError outcome, No rate limiting or anomaly detection layered on repeated verify_chain failures for a given presenter/identity

Existing Controls: Highly granular AuthorityError enum (14 variants) provides rich diagnostic detail if logged • Doc comment explicitly notes verifier logs are 'where an escalation attempt becomes visible', signaling intended design reliance on caller-side logging

Recommended Mitigations: Provide an optional tracing/logging hook or callback parameter in verify_chain (or a wrapping function) that emits structured audit events on every Err path with presenter/scope/action context • Document as a mandatory integration requirement that all AuthorityError returns be logged to an append-only, tamper-evident audit store • Add anomaly detection/alerting on repeated distinct AuthorityError types from the same presenter within a short time window


🔵 STRIDE-7: Information Disclosure via Verbose AuthorityError Detail Fields Exposed to Untrusted Presenters

Field Detail
Category Information Disclosure
Severity Low
Likelihood Likely
CVSS 3.5 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-209,CWE-200
CAPEC CAPEC-54
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: AuthorityError variants in COMP-001 allow internal identity/topology disclosure due to error messages embedding raw internal identifiers (root_issuer, expected governing party, parent subject, actual audience) via #[error(...)] thiserror formatting, resulting in information disclosure to an unauthenticated presenter if these error strings are returned verbatim in an API response rather than being sanitized for external consumption

Evidence: src/authority.rs:~110-120

#[error(\n    \"chain root was issued by `{root_issuer}`, not by `{expected}` which governs the scope\"\n)]\nRootNotGoverning { root_issuer: String, expected: String },

Attack Scenario:

  1. Attacker presents a deliberately malformed or mismatched chain to a service wrapping verify_chain (EP-001), e.g., omitting an audience match or naming a wrong parent.
  2. verify_chain returns Err(AuthorityError::WrongAudience{index, audience, presenter}) or Err(AuthorityError::IssuerNotParentSubject{index, issuer, subject}), whose Display (via thiserror #[error(...)]) embeds the actual internal audience, subject, or governing_party identifiers that were expected.
  3. If the calling web service or API surfaces this Display string directly in an HTTP error response body (a common anti-pattern for thiserror-derived errors), the attacker learns legitimate internal identifiers (e.g., the exact DID of the governing party or a legitimate subject) that were not previously known.
  4. Attacker uses these disclosed identifiers to refine subsequent forged credential chains (e.g., now targeting the correct governing_party string discovered from the error) or to conduct social engineering / further enumeration against the disclosed identities.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: DF-authority-chain-verification

Preconditions: Calling application propagates AuthorityError's Display/Debug output directly into an external-facing response without sanitization, Attacker has any ability to trigger verify_chain with attacker-influenced but distinguishable inputs (e.g., varying the wrong-audience/issuer fields) to enumerate distinct error responses

Existing Controls: Errors are strongly typed enum variants rather than free-form strings, enabling a caller to easily map to sanitized external messages if they choose to • thiserror derives standard Display formatting only, not automatic logging or network exposure

Recommended Mitigations: Document that AuthorityError Display strings are for internal diagnostic/logging use only and MUST NOT be returned verbatim in external API responses • Provide a sanitized external_message() method or a From-conversion to a generic 'access denied' error type for public-facing responses • Add a code-review/lint rule or test asserting no route handler directly serializes AuthorityError text to end-users


🟡 STRIDE-8: Unbounded Attenuation via DTGCredential::attenuate Absent Depth/Rate Enforcement Prior to Verification

Field Detail
Category Denial of Service, Tampering
Severity Medium
Likelihood Possible
CVSS 5.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-770,CWE-400
CAPEC CAPEC-125,CAPEC-130
OWASP A04:2021 - Insecure Design

Description: DTGCredential::attenuate in COMP-002 allows creation of maximal-depth or resource-heavy chains prior to any verify_chain enforcement due to attenuate itself (per available evidence in src/create.rs diff) not visibly enforcing MAX_CHAIN_DEPTH or actions-list-size limits at construction time, resulting in proliferation of large/deep chains that only fail at the final verify_chain step, wasting resources and enabling scenario STRIDE-3 amplification if attenuate is exposed as a network-facing operation

Evidence: src/create.rs:241-404 (truncated diff)

/// Creates a new Verifiable Authority Credential (VAC) — a chain root.\n/// ... `actions` MUST NOT be empty ...\n(attenuate() body not shown in provided diff/source)

Attack Scenario:

  1. A legitimate or attacker-controlled holder repeatedly calls DTGCredential::attenuate(...) (EP-004) on a VAC they hold, each time producing a new narrower-looking but still large credential object.
  2. Because the depth/size constraints (MAX_CHAIN_DEPTH=8, non-empty actions) are enforced only inside verify_chain (per the provided authority.rs) and the create.rs diff shows only an empty-actions constructor check, nothing stops a holder from generating and storing/transmitting arbitrarily many attenuated credentials or chains that exceed practical depth before any verifier ever rejects them.
  3. Attacker floods a credential-issuance or storage endpoint with such over-generated attenuated credentials, consuming storage/bandwidth resources, and only at the eventual verify_chain call (if ever reached) are they rejected with TooDeep.
  4. This produces an asymmetric cost: cheap for attacker to generate, but each stored/transmitted artifact and eventual (possibly never-triggered) verification consumes disproportionate downstream resources.

🔎 Threat Clue: Derived from COMP-002 via EP-004

  • Data Flows: DF-credential-attenuation

Preconditions: attenuate() is reachable by a holder without a pre-check against MAX_CHAIN_DEPTH or without validating resulting chain size before storage/transmission, No storage-layer or transport-layer size/depth limit exists ahead of verify_chain, src/create.rs full content not available for definitive confirmation — inferred from truncated diff showing only an empty-actions check for the new VAC/VDC constructors

Existing Controls: verify_chain enforces MAX_CHAIN_DEPTH=8 as a hard backstop at the trust-decision point • Constructor rejects empty actions lists (per diff notes) preventing one particular meaningless-grant case

Recommended Mitigations: Add depth/size validation directly inside DTGCredential::attenuate (or a pre-attenuation check against the accumulating chain) so oversized chains are rejected at creation time, not only at verification time • Enforce storage/transport-layer size caps on credential chain artifacts ahead of any verify_chain call • Add tests in tests/authority_chain.rs asserting attenuate() itself refuses to produce a chain that would exceed MAX_CHAIN_DEPTH


🟡 STRIDE-9: Type-Confusion Bypass via Non-Exhaustive DTGCredentialType Matching in Downstream Callers

Field Detail
Category Tampering, Elevation of Privilege
Severity Medium
Likelihood Possible
CVSS 5.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-440,CWE-697
CAPEC CAPEC-698
OWASP A08:2021 - Software and Data Integrity Failures

Description: DTGCredentialType (COMP-002) marked #[non_exhaustive] allows silent authorization bypass in downstream consumers due to wildcard match arms added before the Authority/Delegation variants existed, resulting in new VAC/VDC credentials being mishandled (e.g., treated as an unrecognized/default case) by any pre-existing caller code that pattern-matches with a catch-all _ => arm predating this 0.6.0 release

Evidence: CHANGELOG.md:~48-50

- `DTGCredentialType` is `#[non_exhaustive]`, so the two new variants are not a breaking\n  change for callers matching with a wildcard arm. Callers matching exhaustively need one arm\n  each.

Attack Scenario:

  1. Prior to 0.6.0, downstream application code pattern-matches on DTGCredentialType with an exhaustive-looking match but a wildcard _ => default_behavior() arm, per the #[non_exhaustive] design noted in the CHANGELOG ('not a breaking change for callers matching with a wildcard arm').
  2. After upgrading the dtg-credentials dependency to 0.6.0, new credentials of type DTGCredentialType::Authority or ::Delegation begin flowing through the system (e.g., presented at EP-002/EP-003).
  3. The unmodified downstream _ => arm silently absorbs these new variants into whatever default behavior existed before Authority/Delegation types were introduced — potentially treating a VAC as an unrecognized/generic credential type and skipping type-specific authorization logic that assumes only the older variants exist.
  4. If that default arm is permissive (e.g., 'treat unknown type as basic, allow read') rather than fail-closed, an attacker presenting a VAC/VDC could have it processed under weaker rules than the newly-introduced Authority-specific rules intend, effectively bypassing the entire authority-chain verification requirement this PR introduces if the caller never routes Authority-typed credentials into verify_chain at all.
  5. This is a supply-chain / dependency-upgrade integration hazard rather than a bug in authority.rs itself, but it directly undermines the security value this PR delivers if downstream integration is not updated in lockstep.

🔎 Threat Clue: Derived from COMP-002 via EP-002, EP-003

  • Data Flows: DF-credential-issuance

Preconditions: Downstream code depends on dtg-credentials and matches on DTGCredentialType with a non-exhaustive wildcard arm, That wildcard arm's default behavior is permissive rather than fail-closed, Downstream code is not updated to explicitly handle Authority/Delegation after upgrading to 0.6.0

Existing Controls: CHANGELOG explicitly documents the non-exhaustive design and calls out the compatibility implication for exhaustive matchers • authority.rs itself defensively checks matches!(link.type_(), DTGCredentialType::Authority) rather than relying on caller pre-filtering

Recommended Mitigations: Recommend (in migration docs) that all downstream matches on DTGCredentialType use fail-closed default arms (reject/deny) rather than permissive defaults • Add a deprecation-cycle warning or clippy lint suggestion for exhaustive-with-wildcard matches on this enum in consuming crates • Provide a migration guide item explicitly calling out the Authority/Delegation addition as security-relevant, not just an API compatibility note


🟡 STRIDE-10: Panic-Based Denial of Service via Violated Internal Invariant in expect() Calls

Field Detail
Category Denial of Service
Severity Medium
Likelihood Possible
CVSS 5.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-248,CWE-617
CAPEC CAPEC-153
OWASP A04:2021 - Insecure Design

Description: verify_chain in COMP-001 allows process-crashing denial of service due to reliance on .expect("checked above") calls that assume a prior matches!/ok_or_else check guarantees authority() returns Some, resulting in an unwrap panic and potential service crash if any code path allows a chain element to reach these lines without having actually passed the per-element loop (e.g., future refactors, feature-flag divergence, or an untested DTGCredentialType::Authority value whose credential().authority() legitimately returns None due to a construction/deserialization edge case not covered by tests/authority_chain.rs)

Evidence: src/authority.rs:~241-244, 290-292

let leaf_grant = leaf.credential().authority().expect(\"checked above\");\n...\nlet root_grant = root.credential().authority().expect(\"checked above\");

Attack Scenario:

  1. Attacker (or a future code change) introduces a data path where a DTGCredential reports type_() == DTGCredentialType::Authority (passing the matches! check) but its credential().authority() returns None in some malformed/edge-case deserialization state not covered by the initial per-element validation loop — for example, via direct construction through DTGCommon::authority_mut mentioned in the CHANGELOG as existing specifically so 'tests can build chains attenuate would refuse.'
  2. If any future refactor reorders the per-element loop relative to the leaf/walk sections, or if a code path calls leaf_grant/grant/parent_grant .expect("checked above") (multiple occurrences in src/authority.rs) on an element that bypassed the initial loop's ok_or_else check due to such a refactor or an alternate entry path, the .expect() panics.
  3. In a service context (e.g., an async request handler), an unhandled panic in a thread without catch_unwind can crash the worker thread/task, and if replicated across concurrent requests, can degrade or crash the verifying service.
  4. Because DTGCommon::authority_mut is explicitly documented as existing to let tests construct otherwise-invalid states, the invariant that 'a chain passing the type check always has Some(authority())' is only as strong as the code paths that reach verify_chain, and is not enforced by the type system itself (still Option<AuthorityGrant>, not a non-optional field).

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: DF-authority-chain-verification

Preconditions: A future code change, alternate entry point, or malformed construction path allows a DTGCredentialType::Authority-tagged credential to reach the walk/root sections without passing the initial per-element loop's authority()-is-Some check, No panic-catching/isolation boundary (e.g., catch_unwind, process-per-request isolation) around verify_chain invocation in the hosting service

Existing Controls: Current control flow does perform the ok_or_else check for every element in the initial loop before any .expect() call is reached, so under the code as currently written, the invariant holds • thiserror-derived error handling elsewhere avoids panics for all expected/normal error conditions

Recommended Mitigations: Replace .expect("checked above") calls with explicit error propagation (ok_or_else(|| AuthorityError::NotAuthority{...})?) to eliminate reliance on cross-loop invariants entirely, making the function panic-free under all inputs by construction • Add a regression test explicitly constructing (via authority_mut) a credential that is type Authority but has authority() == None, feeding it into every code path of verify_chain to confirm no panic occurs • Wrap verify_chain invocation at the service boundary with panic isolation (catch_unwind or per-request task isolation) as defense-in-depth


🟡 STRIDE-11: Audience-Binding Bypass via Absent Audience Enforcement on Non-Leaf Chain Links

Field Detail
Category Spoofing
Severity Medium
Likelihood Possible
CVSS 6.4 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-287,CWE-863
CAPEC CAPEC-593
OWASP A01:2021 - Broken Access Control

Description: verify_chain in COMP-001 allows unauthorized presentation of intermediate credentials due to the WrongAudience check being applied only to chain[0] (the leaf) via leaf_grant.audience, resulting in bypass of audience binding for any non-leaf link that separately sets its own audience field, since intermediate links' audience constraints are never validated against presenter

Evidence: src/authority.rs:~204-212

let leaf = &chain[0];\nlet leaf_grant = leaf.credential().authority().expect(\"checked above\");\nif let Some(audience) = &leaf_grant.audience\n    && audience != presenter\n{\n    return Err(AuthorityError::WrongAudience { index: 0, ... });\n}

Attack Scenario:

  1. A legitimate credential holder A obtains an intermediate VAC (position 1..n-1 in a future chain) that is audience-bound to a specific delegate D via that intermediate link's own audience field, intended so only D may ever present a chain containing that link.
  2. Attacker (not D) somehow obtains this intermediate link plus a compatible leaf/child link chaining from it (e.g., via credential leakage, insider threat, or a compromised storage system) and constructs a full chain with themselves as presenter.
  3. verify_chain in src/authority.rs only checks if let Some(audience) = &leaf_grant.audience && audience != presenter against chain[0] — no equivalent check exists in the leaf-to-root walking loop for parent_grant.audience or any intermediate link's own audience field.
  4. The attacker's chain passes verify_chain despite the intermediate link being audience-bound to someone else, because only the leaf's audience binding is enforced, not every link's.
  5. Attacker obtains a VerifiedAuthority result and is granted the scope/actions the chain confers, defeating the audience-binding protection for all non-leaf positions.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: DF-authority-chain-verification

Preconditions: A chain contains an intermediate (non-leaf) link with its own audience field set to a value other than the current presenter, Attacker has obtained such an intermediate link plus a valid child link chaining from it, through leakage or a compromised distribution channel, Design intent (per module doc: 'audience, where set, must be the presenter') is understood to apply to every link, not only the leaf, but the code only enforces it for chain[0]

Existing Controls: Leaf audience binding is enforced, mitigating the most common single-hop leaked-credential replay case • IssuerNotParentSubject check still requires correct issuer/subject chaining regardless of audience

Recommended Mitigations: Extend the per-link loop (or the initial per-element loop) to check grant.audience against presenter for every link in the chain, not only the leaf • Add a specific regression test in tests/authority_chain.rs presenting a chain where an intermediate (non-leaf) link is audience-bound to a different party than the presenter, asserting rejection • Clarify the module doc/spec draft (PR #29) on whether audience binding is leaf-only by design or should apply chain-wide, and align code to that explicit decision


🟡 STRIDE-12: Scope String Ambiguity Enabling Hierarchical Scope Confusion in WidensScope Check

Field Detail
Category Tampering, Elevation of Privilege
Severity Medium
Likelihood Possible
CVSS 5.7 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-863,CWE-697
CAPEC CAPEC-680
OWASP A01:2021 - Broken Access Control

Description: verify_chain in COMP-001 allows scope-narrowing bypass due to scope being an opaque String compared with exact != equality (grant.scope != parent_grant.scope) rather than a structured hierarchical scope model, resulting in potential authority misapplication if the calling system treats scope strings as hierarchical/prefix-matched (e.g., org/team implying access to org/team/sub) while verify_chain enforces only byte-for-byte equality, creating a mismatch between the verifier's narrowing guarantee and the resource server's actual authorization semantics

Evidence: src/authority.rs:~267-273

if grant.scope != parent_grant.scope {\n    return Err(AuthorityError::WidensScope { index, scope: grant.scope.clone(), parent_scope: parent_grant.scope.clone() });\n}

Attack Scenario:

  1. Resource server / policy enforcement point treats scope strings hierarchically, e.g., granting access to any resource path prefixed by the scope string (a common real-world pattern for path- or namespace-based scopes).
  2. Root VAC issued by governing party sets scope = \"org/finance\". A legitimate attenuated child sets scope = \"org/finance\" too (unchanged, passes WidensScope's exact equality), which is expected and fine.
  3. However, because verify_chain's WidensScope check is exact-string equality rather than hierarchy-aware, it cannot distinguish 'same scope, no widening' from 'differently-formatted but semantically identical or overlapping scope' — an attacker who can influence how scope strings are serialized (e.g., trailing slash org/finance/ vs org/finance, or case) could potentially get a chain accepted by verify_chain (because the string happens to match through some formatting coincidence introduced upstream) that the resource server then interprets more broadly than the governing party intended, since the two systems (verifier and resource server) do not share the same scope-comparison semantics.
  4. This is a semantic mismatch/confusion vulnerability: verify_chain provides only exact-match narrowing, but says nothing about how consuming policy engines interpret the resulting scope string, allowing scope semantics to diverge from the intended strict narrowing guarantee across system boundaries.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: DF-authority-chain-verification

Preconditions: Downstream policy enforcement treats scope as hierarchical/prefix-matched rather than as an exact opaque token, Scope string formatting is not strictly canonicalized before being set into a credential (e.g., trailing separators, case), Attacker has some influence over scope string formatting during credential issuance or attenuation (e.g., via a shared library bug or user-supplied scope names)

Existing Controls: Exact-string WidensScope check does correctly prevent all literal scope changes across a link boundary • Design intent (per module doc) is explicit that scope must not widen, which the exact-match implementation correctly enforces at the string level

Recommended Mitigations: Canonicalize scope strings (e.g., strip trailing separators, enforce case, define a formal grammar) at credential construction time so exact-match semantics align with any hierarchical interpretation used downstream • Document explicitly that scope is treated as an opaque, exact-match token by this verifier and that any hierarchical interpretation must be implemented consistently by the resource server using the same canonicalization • Add integration tests spanning both the credential library and a representative policy-enforcement consumer to catch scope-semantics drift



🍝 PASTA Threat Model

Application Purpose

dtg-credentials is a Rust library implementing Decentralized Trust Graph (DTG) verifiable credentials, and this PR adds Verifiable Authority Credentials (VAC) and Verifiable Delegation Credentials (VDC) with a chain-verification function so that decentralized systems can safely delegate and attenuate scoped authority without any party being able to self-grant privileges.

Inherent Risks

  • The library explicitly defers signature verification to a separate, unshown data-integrity suite, creating an integration-order dependency that consumers may get wrong.
  • Trust decisions rely on caller-supplied strings (governing_party, presenter, timestamps) whose authenticity and normalization are outside this module's control.
  • The module is explicitly tied to unapproved, moving draft specifications (trustoverip/dtgwg-cred-spec PR #29 and ci: check for an already-published version before authenticating #19), so the security model itself may change before stabilization.
  • Full source (src/lib.rs, complete src/create.rs, tests/authority_chain.rs) was not available for review, limiting confidence in whether upstream callers correctly invoke verify_chain and signature verification together.

Objectives

Risk: Treat any bypass of the seven documented chain-narrowing invariants as a critical risk equivalent to full authority forgery.
Business: Enable decentralized, cryptographically-grounded delegation of authority between organizations, agents, and sub-agents without a central authorization server.
Security: Guarantee that no chain of attenuations can ever grant more authority than the governing party's root credential conferred.; Ensure verification never depends on network resolution of parent credentials (bearer-side resolution only).
Financial: Avoid costly security incidents (privilege escalation, credential forgery) that would damage trust in the DTG credential ecosystem and its adopters.
Compliance: Align eventual behavior with the trustoverip/dtgwg-cred-spec draft (PR #29, #19) once ratified, without introducing security regressions during the draft period.
Functional: Provide constructors for VAC/VDC credentials and a chain-attenuation mechanism (attenuate) plus a verifier (verify_chain) that enforces narrowing invariants.
Operational: Keep chain verification performant and bounded (MAX_CHAIN_DEPTH) so it can run on every credential presentation without becoming an availability bottleneck.

Business Impact Analysis (2)

BIA-1: Authority Chain Verification Service (Critical)

The end-to-end process by which a credential holder presents an authority chain and a relying party calls verify_chain (plus separate signature verification) to decide whether to grant a requested scope/action.

MTD: 00 days 04:00 hours | RTO: 00 days 01:00 hours | RPO: 00 days 00:15 hours

  • Stakeholders: Credential Holders / Governing Parties / Relying Parties / Security/Compliance Team
  • Dependencies: chrono crate for timestamp handling / serde/serde_json for credential deserialization / thiserror for error typing / Unshown signature/data-integrity verification suite / Unshown src/lib.rs and src/create.rs full implementation
  • Disruptions: Signature verification step is skipped or misordered by an integrating application / Chain depth or actions-list size is used to exhaust verifier CPU / Caller supplies a manipulated or stale at timestamp / Non-exhaustive enum matching in downstream code silently mishandles new credential types
  • Impacts: Complete authority forgery allowing unauthorized access to any scope/action (financial and reputational) / Denial of service against the verification path degrading availability of every relying party using it / Regulatory/compliance exposure if the credential system is used for access to regulated resources

BIA-2: Credential Attenuation Issuance (High)

The process by which a credential holder derives a narrower VAC from one they hold via DTGCredential::attenuate, without contacting the original issuer.

MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 01:00 hours

  • Stakeholders: Credential Holders / Agent/Sub-Agent Delegates / Relying Parties
  • Dependencies: src/create.rs constructor logic / AuthorityGrant structure
  • Disruptions: Attenuate produces oversized or maximal-depth chains that are only rejected later at verify_chain / Attenuate lacks the same rigor as verify_chain for enforcing narrowing at creation time
  • Impacts: Resource waste from over-generated invalid chains / Delayed detection of misuse until the verification boundary

Technical Scope

Roles (3): RO-1 Governing Party (Root Issuer) · RO-2 Credential Holder / Delegate · RO-3 Relying Party / Verifier Operator

Actors (3): AC-1 Human Delegate / Agent User · AC-2 Relying Party Service · AC-3 Governing Party Issuance System

Entry Points (4): EP-001 verify_chain Function Call · EP-002 new_vac Constructor Call · EP-003 new_vdc Constructor Call · EP-004 attenuate Method Call

Threat Actors (3): TA-1 Malicious Credential Holder · TA-2 External Network Attacker · TA-3 Insider / Compromised Integrator

Infrastructure (1): IF-1 Rust Library Crate (dtg-credentials)

Trust Boundaries (3): TB-1 Untrusted Credential Presentation Boundary · TB-2 Library-Internal Trust Boundary · TB-3 Governing Party Root-of-Trust Boundary

External Entities (2): EE-1 Credential Holder / Presenter · EE-2 Governing Party

System Components (4): SC-1 Authority Chain Verifier (authority.rs) · SC-2 Credential Constructor/Attenuator (create.rs) · SC-3 Credential Data Model (lib.rs, not fully shown) · SC-4 Integrating Relying-Party Application

Resources And Assets (3): RA-1 Authority Chain (Vec) · RA-2 VerifiedAuthority Result · RA-3 AuthorityGrant (scope, actions, parent, audience)

Technologies And Dependencies (3): TD-1 chrono · TD-2 thiserror · TD-3 serde / serde_json

Use Cases (3)

  • Governing Party Issues a Root Authority Credential: The governing party for a scope uses DTGCredential::new_vac to mint a root VAC naming the scope and actions it grants, establishing the trust anchor that all attenuated chains must ultimately trace ba
  • Holder Attenuates a Narrower Delegation Credential: A credential holder calls DTGCredential::attenuate on a VAC they hold to derive a narrower VAC for an agent or sub-agent, without contacting the original issuer, enabling scoped delegation such as a m
  • Relying Party Verifies a Presented Authority Chain: A relying party service receives a full authority chain from a holder along with a requested scope and action, verifies each credential's signature separately, then calls authority::verify_chain to co

⚔️ Attack Scenarios (2)

SC-1: Authority Chain Verifier (authority.rs)

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC1n@{ shape: rect, label: "SC-1: Authority Chain Verifier" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE345@{ shape: rect, label: "CWE-345: Insufficient Verification of Data Authenticity" }
    CWE294@{ shape: rect, label: "CWE-294: Authentication Bypass by Capture-replay" }
    CWE178@{ shape: rect, label: "CWE-178: Improper Handling of Case Sensitivity" }
    CWE287@{ shape: rect, label: "CWE-287: Improper Authentication" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC115@{ shape: rect, label: "CAPEC-115: Authentication Bypass" }
    CAPEC90@{ shape: rect, label: "CAPEC-90: Reflection Attack in Authentication Protocol" }
    CAPEC627@{ shape: rect, label: "CAPEC-627: Counterfeit GPS Signals (Identifier Spoofing Analogy)" }
    CAPEC593@{ shape: rect, label: "CAPEC-593: Session Hijacking" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    T1@{ shape: rect, label: "STRIDE-1: Signature Verification Omission<br><i>Critical / Likely</i>" }
    T5@{ shape: rect, label: "STRIDE-5: Clock-Trust Manipulation<br><i>High / Likely</i>" }
    T2@{ shape: rect, label: "STRIDE-2: String-Based Identity Comparison Spoofing<br><i>High / Possible</i>" }
    T11@{ shape: rect, label: "STRIDE-11: Audience-Binding Bypass<br><i>Medium / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA1@{ shape: rect, label: "TA-1: Malicious Credential Holder<br><i>Escalate authority beyond grant</i>" }
    TA3@{ shape: rect, label: "TA-3: Insider / Compromised Integrator<br><i>Exploit integration mistakes</i>" }
  end
  SC1n --> CWE345 --> CAPEC115 --> T1 --> TA1
  SC1n --> CWE294 --> CAPEC90 --> T5 --> TA3
  SC1n --> CWE178 --> CAPEC627 --> T2 --> TA1
  SC1n --> CWE287 --> CAPEC593 --> T11 --> TA1
  linkStyle 0 stroke:#A50000, stroke-width:2px
  linkStyle 1 stroke:#A50000, stroke-width:2px
  linkStyle 2 stroke:#A50000, stroke-width:2px
  linkStyle 3 stroke:#A50000, stroke-width:2px
  linkStyle 4 stroke:#FF0000, stroke-width:2px
  linkStyle 5 stroke:#FF0000, stroke-width:2px
  linkStyle 6 stroke:#FF0000, stroke-width:2px
  linkStyle 7 stroke:#FF0000, stroke-width:2px
  linkStyle 8 stroke:#FF0000, stroke-width:2px
  linkStyle 9 stroke:#FF0000, stroke-width:2px
  linkStyle 10 stroke:#FFA500, stroke-width:2px
  linkStyle 11 stroke:#FFA500, stroke-width:2px
  linkStyle 12 stroke:#FFA500, stroke-width:2px
  linkStyle 13 stroke:#FFA500, stroke-width:2px
  linkStyle 14 stroke:#FFA500, stroke-width:2px
  linkStyle 15 stroke:#FFA500, stroke-width:2px
Loading

SC-2: Credential Constructor/Attenuator (create.rs)

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC2n@{ shape: rect, label: "SC-2: Credential Constructor/Attenuator" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE770@{ shape: rect, label: "CWE-770: Allocation of Resources Without Limits" }
    CWE440@{ shape: rect, label: "CWE-440: Expected Behavior Violation" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC125@{ shape: rect, label: "CAPEC-125: Flooding" }
    CAPEC698@{ shape: rect, label: "CAPEC-698: Install Malicious Extension (Type Confusion Analogy)" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    T8@{ shape: rect, label: "STRIDE-8: Unbounded Attenuation<br><i>Medium / Possible</i>" }
    T9@{ shape: rect, label: "STRIDE-9: Type-Confusion Bypass<br><i>Medium / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA2@{ shape: rect, label: "TA-2: External Network Attacker<br><i>Disrupt availability</i>" }
    TA3b@{ shape: rect, label: "TA-3: Insider / Compromised Integrator<br><i>Exploit integration mistakes</i>" }
  end
  SC2n --> CWE770 --> CAPEC125 --> T8 --> TA2
  SC2n --> CWE440 --> CAPEC698 --> T9 --> TA3b
  linkStyle 0 stroke:#FFA500, stroke-width:2px
  linkStyle 1 stroke:#FFA500, stroke-width:2px
  linkStyle 2 stroke:#FFA500, stroke-width:2px
  linkStyle 3 stroke:#FFA500, stroke-width:2px
  linkStyle 4 stroke:#FFA500, stroke-width:2px
  linkStyle 5 stroke:#FFA500, stroke-width:2px
  linkStyle 6 stroke:#FFA500, stroke-width:2px
  linkStyle 7 stroke:#FFA500, stroke-width:2px
Loading

📊 Risk Summary

Total Threats: 12

By Severity: Low: 1 · High: 2 · Medium: 8 · Critical: 1

By Category: Spoofing: 3 · Elevation of Privilege: 5 · Denial of Service: 3 · Tampering: 5 · Repudiation: 1 · Information Disclosure: 1

🎯 Attack Surface

Kill Chain 1: The most severe chain begins at EP-001 (verify_chain) where an attacker exploits the documented but easily-missed integration gap (STRIDE-1) that signatures are never checked inside authority.rs itself; combined with STRIDE-2's lack of identifier normalization on the RootNotGoverning check and STRIDE-5's reliance on a caller-supplied at timestamp, an attacker who controls the surrounding integration (or exploits a sloppy one) can present a fully self-issued, unsigned, or expired chain that nonetheless returns Ok(VerifiedAuthority), achieving complete authority forgery against any relying party that has not layered independent signature verification and trusted clock sourcing in front of this library call. Kill Chain 2: A secondary chain combines STRIDE-11 (audience binding enforced only on the leaf) with STRIDE-4 (no global duplicate-id detection) — an attacker who obtains a leaked intermediate credential bound to a different audience can graft it into a fresh chain terminating in their own presenter identity, and because non-adjacent duplicate ids are never checked, a poorly-designed credential cache in the relying party's SC-4 integration layer could compound the confusion by re-resolving a stale or substituted credential object post-verification, silently widening the effective authority granted. Kill Chain 3: A resource-exhaustion chain starts with STRIDE-8 (attenuate producing oversized/near-maximal-depth chains with no early rejection) feeding into STRIDE-3 (the O(depth × actions²) nested scan in the leaf-to-root walk) and STRIDE-10 (panic-prone .expect() calls that could be reached via edge-case constructions using the test-only authority_mut accessor if it is ever exposed beyond tests) — together enabling a low-cost attacker to degrade or crash the relying party's verification service through crafted, maximal-size, and possibly malformed credential chains submitted at volume against EP-001. Kill Chain 4: A supply-chain/deployment-timing chain links STRIDE-9 (non-exhaustive enum silently absorbed by pre-existing wildcard match arms in downstream code) with the overall PR's introduction of Authority/Delegation types — during the window between a dependency version bump and a downstream code update, VAC/VDC credentials may flow through legacy code paths that never invoke verify_chain at all, meaning the entire security mechanism this PR introduces can be bypassed simply by the surrounding ecosystem's lag in adopting it, independent of any bug in authority.rs itself.

🛡️ Risk Mitigation Strategy

Priority 1 (Immediate): The single highest-leverage gap is that authority.rs's core guarantee (no chain can widen authority) is only half of the security model — signature verification is explicitly out of scope and left to callers (STRIDE-1), and the trust anchor comparison (STRIDE-2) and temporal trust (STRIDE-5) both rely on caller-supplied, unauthenticated inputs. Mitigation must prioritize converting these from documentation-only contracts into type-system-enforced contracts: a VerifiedCredential type-state that can only be constructed post-signature-check, removal of the caller-supplied at parameter in favor of an internally-sourced trusted clock (or explicit, tested guidance plus replay protection at the call site), and canonicalized identifier comparison for governing_party/issuer/subject/audience fields. Priority 2 (Short-Term): Close the audience-binding gap for non-leaf links (STRIDE-11) and add duplicate-id detection across the whole chain (STRIDE-4), since both are localized, low-effort code changes within authority.rs's existing loop structure and materially reduce the blast radius of any leaked intermediate credential. Priority 3 (Medium-Term): Harden availability by enforcing size/length limits on actions lists at construction time (addressing STRIDE-3 and STRIDE-8 together), replacing .expect("checked above") calls with explicit error propagation to make the function panic-free by construction regardless of future refactors (STRIDE-10), and adding structured audit-logging hooks so every AuthorityError outcome can be correlated to a presenter identity for anomaly detection (STRIDE-6). Priority 4 (Long-Term): Address ecosystem-level risks that outlive this specific PR — publish explicit migration guidance warning downstream integrators that permissive wildcard matches on the now-non-exhaustive DTGCredentialType enum can silently bypass the new authority model (STRIDE-9), sanitize AuthorityError Display output before it ever reaches an external


Generated by Agentic Sec — Threat Model & Affect Analysis Agent

📊 Summary & findings
✅ Confirmed ⚠️ Must-Review-By-Human
2 3

Confirmed (2)

  • 🟡 Validity-window checks trust a fully caller-supplied timestamp, enabling temporal bypass if at is attacker-influenced (triaged HIGH→MEDIUM)
  • 🟡 Non-canonicalized string equality on identity fields (issuer/subject/audience) risks spoofing if identifiers are DIDs/URIs with encoding variance

Must-Review-By-Human (3)

  • 🟠 Hardcoded Secrets: Generic Api Key (2 occurrences in 1 unique secrets)
  • UNCONFIRMED — Hardcoded Secret: Generic Api Key in lib.rs:923
  • UNCONFIRMED — Hardcoded Secret: Generic Api Key in lib.rs:955

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants